﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace NTypeClasses
{
	/// <summary>
	/// Some useful methods for type class support.
	/// </summary>
	public static class TypeClasses
	{
		private static readonly Dictionary<Assembly, Type[]> extensionCache =
			new Dictionary<Assembly, Type[]>();

		/// <summary>
		/// Gets all type class extension types defined in specified assembly.
		/// </summary>
		/// <param name="assembly">An assembly to get extension types from.</param>
		/// <returns>All type class extension types defined in assembly.</returns>
		public static Type[] GetExtensions(Assembly assembly)
		{
			lock (extensionCache) {
				Type[] temp;
				if (extensionCache.TryGetValue(assembly, out temp))
					return (Type[])temp.Clone();

				temp = assembly.GetExportedTypes().Where(IsExtensionType).ToArray();
				extensionCache[assembly] = (Type[])temp.Clone();
				return temp;
			}
		}

		private static bool IsExtensionType(Type type)
		{
			return type.GetCustomAttributes(typeof(TypeExtensionAttribute), false).Length > 0;
		}

		/// <summary>
		/// Finds an implementation for specified method of type class element.
		/// </summary>
		/// <param name="t">Type class element.</param>
		/// <param name="method">Method to find implementation for.</param>
		/// <returns>An implementation for method or null.</returns>
		public static MethodInfo GetMethodImplementation(Type t, MethodInfo method)
		{
			var own = t.GetGenericArguments()
				.SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance))
				.Select(m => MatchSignature(method, m))
				.FirstOrDefault(m => m != null);
			if (own != null) return own;

			var extensions = AppDomain.CurrentDomain.GetAssemblies()
				.Where(assembly => assembly.DependsOn(Assembly.GetExecutingAssembly())
					|| assembly == Assembly.GetExecutingAssembly())
				.SelectMany(GetExtensions);

			return extensions
				.SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Static))
				.Select(m => MatchSignature(method, m))
				.SingleOrDefault(m => m != null);
			// TODO: GetMethodImplementation
		}

		private static Type Unify(Type actual, Type generic, IDictionary<Type, Type> unificationDict)
		{
			if (generic.IsAssignableFrom(actual)) return generic;

			if (generic.IsGenericParameter) {
				Type substitution;
				if (unificationDict.TryGetValue(generic, out substitution)) {
					return Unify(actual, substitution, unificationDict);
				}
				unificationDict[generic] = actual;
				return actual;
			}

			var actualGeneric = actual.GetGenericTypeDefinition();
			var genericGeneric = generic.GetGenericTypeDefinition();
			if (genericGeneric != actualGeneric) return null;

			var actualArgs = actual.GetGenericArguments();
			var genericArgs = generic.GetGenericArguments();
			if (actualArgs.Length != genericArgs.Length) return null;

			for (int i = 0; i < actualArgs.Length; i++) {
				var actualArg = actualArgs[i];
				var genericArg = genericArgs[i];
				genericArgs[i] = Unify(actualArg, genericArg, unificationDict);
				if (genericArgs[i] == null) return null;
			}

			return actual;
		}

		private static MethodInfo Unify(MethodInfo method, MethodInfo m)
		{
			var genericParams = new Dictionary<Type, Type>();

			var need = method.GetParameters().Select(p => p.ParameterType).ToArray();
			var given = m.GetParameters().Select(p => p.ParameterType).ToArray();
			if (!m.IsStatic)
				given = new[]{ m.DeclaringType }.Concat(given).ToArray();

			if (need.Length != given.Length) return null;

			if (need.Select((t, i) => Unify(t, given[i], genericParams))
				.Any(commonType => commonType == null)) {
				return null;
			}
			var retCommon = Unify(method.ReturnType, m.ReturnType, genericParams);
			if (retCommon == null) return null;

			if (!m.IsGenericMethod) return m;

			var genericArgs = m.GetGenericArguments()
					.Where(genericParams.ContainsKey)
					.Select(arg => genericParams[arg])
					.ToArray();
			m = m.MakeGenericMethod(genericArgs);

			return m;
		}

		private static MethodInfo MatchSignature(MethodInfo method, MethodInfo m)
		{
			if (method.Name != m.Name) return null;
			return Unify(method, m);
		}

		/// <summary>
		/// NTypeClasses assembly.
		/// </summary>
		public static Assembly Assembly { get { return Assembly.GetExecutingAssembly(); } }
	}
}
